[Next.js 迁移 Cloudflare Workers]:从 Pages 到 OpenNext 的完整排查与重构实录
随着项目的迭代,Cloudflare 免费版 3MB 的脚本体积限制成了我应用上线的“物理障碍”。为了突破限制并获得更完整的 Node.js 生态支持,我决定将 Next.js 项目从旧的 Cloudflare Pages 模式(
@cloudflare/next-on-pages)全面迁移到官方主推的 Workers 模式(@opennextjs/cloudflare)。本以为只是换个部署命令,没想到却是一次底层架构的彻底重构。本文完整记录了迁移过程中遭遇的 404 错误、环境变量失效、SSG 构建崩溃等致命暗坑,并给出了最终的工程化解决方案。如果你也准备拥抱 OpenNext,这篇文章能帮你少走很多弯路。
1. 事件起因:物理限制引发的重构
在开发我的微型项目(包含 D1 数据库和 R2 存储)时,打包后的体积超出了 Cloudflare 免费版 3MB 的限制。
最初的两个小时,我陷入了“过度工程”的泥潭:尝试移除依赖、手动混淆代码、修改打包配置。但我很快意识到,为了一个常规项目去抠这几兆的体积是隐性的技术债。
我的决定:
- 订阅 5 美元/月的 Workers 付费计划,用物理手段消除体积障碍(付费版单文件上限远超 10MB)。
- 既然付费解锁了完整的生态能力,技术栈也必须跟上:废弃已被官方标记为 Deprecated 的
@cloudflare/next-on-pages,全面迁移到专为 Workers 设计的@opennextjs/cloudflare(即 OpenNext 架构)。
2. 原理剖析:新旧架构的核心差异
导致后续一系列踩坑的根本原因,是我带着“旧地图”去探索“新大陆”。在动手修复之前,必须先理清这两套架构的本质区别:
| 维度 | ❌ 旧方案 (next-on-pages + Pages) |
✅ 新方案 (OpenNext + Workers) |
|---|---|---|
| 部署实体 | Cloudflare Pages 项目 | Cloudflare Worker 项目 |
| 路由特性 | 强依赖 export const runtime = 'edge' |
解锁完整 Node.js 兼容性,无需 edge 声明 |
| 产物结构 | 静态文件 + /.vercel/output/static |
单一入口 .open-next/worker.js + assets 静态资源 |
| 环境变量 | 直接塞入 request.env |
标准化,需通过 getCloudflareContext() 异步获取 |
| CI/CD 分支 | 原生支持:非 main 分支自动切 Preview 环境 | 极度机械:默认不识别分支,需手动传 --env 参数或用 GitHub Actions 接管 |
3. 踩坑与排查实录
在长达一天的迁移过程中,我结结实实地踩了 5 个坑。以下是真实的案发现场与排查过程。
踩坑 1:架构错位导致的 404 幽灵
现象:
代码成功构建,日志显示上传了 46 个文件,但访问域名直接报 HTTP ERROR 404。
排查:
查看本地构建的产物树状图(tree .open-next),发现生成了 worker.js 和 assets 目录,但 assets 里根本没有 index.html。
由于我还在使用 Cloudflare Pages 项目来承载部署,Pages 默认去寻找静态入口文件。它虽然把静态资源传上去了,但**根本没有运行我的 worker.js** 来处理 Next.js 的动态路由。
解决:
彻底废弃旧的 Pages 项目,在控制台新建一个 Worker 项目。
修改 wrangler.toml,从 Pages 配置改为标准的 Workers 资源绑定模式:
# 移除旧的 pages_build_output_dir
main = ".open-next/worker.js"
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
踩坑 2:被时代抛弃的 Edge 限制
现象:
在执行 npx @opennextjs/cloudflare build 时,终端抛出大量路径解析错误:Could not resolve "./cloudflare/images.js"。
排查:
旧项目中为了迎合 Vercel/Pages 的要求,大量文件顶部写了 export const runtime = 'edge'。在 OpenNext 架构下,工具链会自动打包并处理环境适配。保留这些 edge 声明反而会干扰打包器,导致依赖树断裂。
解决:
全局搜索并彻底删除了所有文件中的 export const runtime = 'edge'。
踩坑 3:旧时代上下文获取方式的失效
现象:
构建时报错:getCloudflareContext has been called in sync mode...
排查:
在 OpenNext 架构中,环境被彻底解耦了。Next.js 的 request 对象回归了纯粹的 Web 标准,里面不再包含任何 Cloudflare 私有变量(以前直接用 request.env?.DB 是行不通的)。必须通过官方提供的上下文函数获取,且由于是在服务端组件中,同步调用会阻断生命周期。
解决:
将获取上下文的方式改为异步:
const { env } = await getCloudflareContext({ async: true });
踩坑 4:Next.js SSG 预渲染遭遇“空数据库”阻击(最致命)
现象:
本地或 CI 执行 npx next build 时,流水线直接崩溃:Failed to load site config from DB, using defaults: Error: D1_ERROR: no such table: site_settings: SQLITE_ERROR
排查:
这是 Next.js 极度执着的性能机制导致的。在 next build 阶段,它会启动一个“幽灵访客”去预渲染静态页面 (SSG)。如果页面里写了读取 D1 数据库的代码,它在此刻就会发起真实查询。
由于构建命令没有携带预览环境变量,它默认连上了我的生产库,而生产库里此刻是一张没有建表的空库,当场崩溃。
尝试与最终解决:
一开始我试图在所有页面加上 export const dynamic = 'force-dynamic' 来跳过静态生成,但依然报错。因为之前的 API 路由还在使用旧的写法,导致获取的 DB 实例为 null。
最终,我采用**“单一事实来源”**的原则进行了彻底重构(详见第 5 节的防御性编程)。
踩坑 5:迷信原生 Git 自动化,导致“测试库失联”
现象:
我想实现 dev 分支连测试库,main 分支连正式库。但我推送 dev 分支后,它依然去读取了正式数据库,导致报错。
排查:
Cloudflare Workers 的内置 Git 触发器极其“机械”。它忽略了我的分支名称,直接无脑执行了控制台里写死的那句 npx wrangler deploy,从而默认读取了 wrangler.toml 最顶层的生产环境配置。
解决:
放弃 Cloudflare 简陋的原生 CI/CD,全面转用 GitHub Actions 接管。在流水线脚本中明确定义输入输出逻辑:主分支执行普通部署,测试分支加上 --env preview 参数。
4. 最终标准解决步骤
经过上述折腾,我整理出了一套标准的迁移和部署流:
- 清理旧债: 全局删除
export const runtime = 'edge'。 - 重构配置: 更新
wrangler.toml,废弃 Pages 字段,改为 Workers + Assets 结构。 - 环境统一: 确保生产库和测试库的表结构完全一致(即使生产库暂时无数据,也要建好空表,防止构建期报错)。
- 接管流水线: 在 GitHub Actions 中配置双轨部署策略。
5. 事后加固:防御性编程
针对“踩坑 4”中 32 处 API 路由读取数据库崩溃的问题,我抽离了一个公共的数据库获取模块 src/lib/db/get-db.ts。
这个模块的核心价值在于**“尊重现实,提供兜底”**。它承认在构建期可能拿不到真实的数据库实例,通过 try-catch 拦截错误并返回安全值,让 next build 能够顺利跑完,而不是让流水线死掉。
// src/lib/db/get-db.ts
import { getCloudflareContext } from '@opennextjs/cloudflare';
export async function getDB() {
try {
const { env } = await getCloudflareContext({ async: true });
if (!env || !env.DB) {
console.warn("未检测到 DB 环境变量。可能处于 next build 预渲染阶段。");
return null;
}
return env.DB;
} catch (error) {
console.warn("获取 Cloudflare Context 失败,已降级处理。", error);
return null; // 提供兜底,防止应用崩溃
}
}
所有 API 路由统一改为调用 const db = await getDB(); if (!db) return ...。
6. 经验总结
这次基础设施的迁移,让我深刻体会到了几个技术原则:
- 奥卡姆剃刀(最简有效): 遇到免费版的体积物理限制,花 5 美元升级比耗费数小时写恶心的代码混淆要高效得多。不要为了规避机制而制造技术债。
- 地图非疆域(认清底层逻辑): 工具的抽象层(如原生的 Git 集成)往往会掩盖底层的真实运作逻辑。当原生工具满足不了多环境路由时,果断废弃它,用 GitHub Actions 掌握绝对的控制权。
- 二阶思维(考虑到“然后呢?”): 代码不能只考虑“运行期”怎么跑,还要考虑到“构建期”怎么跑。Next.js 的 SSG 机制就是典型的例子,防御性编程和单例模式的兜底是极其必要的基建工作。
附:速查配置清单
1. 标准的 OpenNext wrangler.toml 模板:
name = "worker-your-project"
main = ".open-next/worker.js"
compatibility_date = "2024-03-20"
compatibility_flags = [ "nodejs_compat" ]
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
# Worker 自引用(OpenNext 必需,不可删除)
[[services]]
binding = "WORKER_SELF_REFERENCE"
service = "worker-your-project"
# 生产环境 D1
[[d1_databases]]
binding = "DB"
database_name = "prod-db"
database_id = "xxxx-xxxx-xxxx"
# 预览环境 D1
[env.preview]
name = "worker-your-project-preview"
[[env.preview.d1_databases]]
binding = "DB"
database_name = "preview-db"
database_id = "yyyy-yyyy-yyyy"
2. 标准部署脚本 (package.json):
"scripts": {
"build": "next build",
"deploy": "opennextjs-cloudflare deploy",
"deploy:preview": "opennextjs-cloudflare deploy --env preview"
}